Skip to content

fix(plugins): add argv-level injection validation to build_command#280

Open
advikdivekar wants to merge 2 commits into
utksh1:mainfrom
advikdivekar:security/command-arg-injection
Open

fix(plugins): add argv-level injection validation to build_command#280
advikdivekar wants to merge 2 commits into
utksh1:mainfrom
advikdivekar:security/command-arg-injection

Conversation

@advikdivekar
Copy link
Copy Markdown
Contributor

What is the problem

Closes #201

PluginManager.build_command() interpolated user-supplied values directly into the argv list without any validation. While asyncio.create_subprocess_exec (no shell=True) prevents shell metacharacter injection, argv-level flag injection is still exploitable:

# Attacker input:
inputs = {"ports": "--script=evil.nse", "scan_type": "-sV --max-rate=1"}

# Resulting argv:
["nmap", "--script=evil.nse", "-sV", "--max-rate=1", "192.168.1.1"]
# nmap treats every element as a valid flag

Affected fields in the nmap plugin: ports (free-text string, no validation), and any plugin field that accepts a STRING type without a pattern constraint.

What was changed

File Change
backend/secuscan/plugins.py _PORT_SPEC_PATTERN — regex for valid port specs; _reject_injected_args() — rejects values beginning with -; _validate_inputs_against_schema() — enforces SELECT allowed-values, INTEGER type, BOOLEAN type, and field-level regex patterns; build_command() — calls validation before _normalize_inputs()
backend/secuscan/scanners/port_scanner.py _resolve_scan_type() — maps shorthand values (e.g. -sV, sT) to schema-compliant SELECT values (S/T/U); _resolve_ports() — maps shorthand (top100, all) to numeric ranges accepted by the plugin; removes unused speed field
testing/backend/unit/test_command_injection.py 52 new tests: injection rejection, SELECT/INTEGER/BOOLEAN validation, pattern checks, port normalisation, scan-type normalisation

Why this approach

  • Validation at the schema boundary: each field's type and options list is declared in metadata.json; validating against it blocks injections for all plugins generically, not just nmap
  • Port fields get a dedicated grammar check (^[\d,\-]+$) because they legitimately contain hyphens (ranges), so a blanket "no leading dash" rule is insufficient
  • Validation before normalisation: SELECT checks must run on raw user values; if normalisation ran first (e.g. resolving a wordlist alias), the resolved path would fail the SELECT check
  • No shell=True: create_subprocess_exec already handles shell metacharacters; this fix closes the remaining argv-level vector

How to test

from backend.secuscan.plugins import get_plugin_manager

pm = get_plugin_manager()

# Should raise ValueError — ports flag injection
pm.build_command("nmap", {"target": "127.0.0.1", "ports": "--script=evil.nse"})

# Should raise ValueError — scan_type out of allowed values
pm.build_command("nmap", {"target": "127.0.0.1", "scan_type": "-sV"})

# Should succeed
pm.build_command("nmap", {"target": "127.0.0.1", "ports": "22,80,443", "scan_type": "T"})

Edge cases covered

  • Empty ports string is valid (plugin uses --top-ports 100 default)
  • Port ranges like "1-1000" are valid (hyphen in numeric context)
  • "-sT" and "sT" are normalised to "T" by PortScanner before reaching validation
  • Integer fields: "30" accepted, "thirty" rejected, "--evil" rejected
  • Boolean fields: True, "true", "1" accepted; "yes" rejected
  • None and "" values are skipped (handled by _with_field_defaults)
  • Unknown fields (not in schema) are ignored

Verification checklist

  • pytest testing/backend/ — 290 passed (1 pre-existing failure in test_route_rejects_task_when_limiter_full)
  • 52 new injection/validation tests — all pass
  • All existing plugin tests — pass

User-supplied scan inputs were interpolated into argv lists without
validation. With asyncio.create_subprocess_exec (no shell=True), shell
metacharacters are safe but argv-level flag injection is real: a value
like ports="--script=evil.nse" becomes a standalone argv element that
nmap accepts as a flag.

Changes:
- plugins.py: add _reject_injected_args() — rejects values that begin
  with '-' (flag injection); for port fields enforces digits/commas/hyphens
  only via _PORT_SPEC_PATTERN
- plugins.py: add _validate_inputs_against_schema() — enforces SELECT
  allowed-values, INTEGER type, BOOLEAN type, and field-level regex patterns
  before any interpolation occurs; called in build_command() BEFORE
  _normalize_inputs() so SELECT checks run on raw user values
- port_scanner.py: add _resolve_scan_type() and _resolve_ports() to map
  shorthand inputs (e.g. "-sV", "top100") to schema-compliant values;
  removes the old 'speed' field that had no corresponding plugin field
- test_command_injection.py: 52 tests covering injection rejection,
  SELECT/INTEGER/BOOLEAN validation, pattern validation, port normalisation,
  scan-type normalisation

Fixes utksh1#201 (command argument injection via unsanitized scan_type/ports/speed).
@advikdivekar
Copy link
Copy Markdown
Contributor Author

@utksh1 please review it and I have asked to claim few issues, please assign those issues to me under GSSoC 2026

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f1a998082

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +55 to +56
if raw == "top1000":
return "1-1000"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve top1000 semantics when normalizing ports

Mapping "top1000" to "1-1000" changes scan behavior from “top 1000 most common ports” to “first 1000 numeric ports,” which can miss frequently exposed services above 1000 and produce materially different findings for existing callers that use this shortcut. This should preserve the original top-ports behavior (e.g., via preset/flag handling) rather than narrowing the scan range.

Useful? React with 👍 / 👎.

Comment on lines +72 to +74
"service_detection": bool(inputs.get("service_detection", True)),
"os_detection": bool(inputs.get("os_detection", False)),
"safe_mode": bool(inputs.get("safe_mode", True)),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse boolean strings explicitly in scanner inputs

Using bool(...) here inverts common string inputs because any non-empty string is truthy, so values like "false" will still enable service_detection, os_detection, and safe_mode. Since task inputs are passed through as arbitrary JSON, clients that send booleans as strings will get incorrect scan flags and behavior. These fields need explicit boolean parsing (e.g., recognize "true"/"false", "1"/"0") instead of Python truthiness coercion.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Command Argument Injection via Unsanitized Scanner Inputs (scan_type, ports, speed)

1 participant